/*
 * Copyright (C) 2017 Fromentin Xavier
 *
 * This file is part of GDOB.
 *
 * GDOB is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * GDOB is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.grimdawnofflinebuilder.util.template.classGeneration

import com.grimdawnofflinebuilder.util.template.templateReader.Group
import com.grimdawnofflinebuilder.util.template.templateReader.Variable
import java.io.File
import java.io.OutputStreamWriter

/**
 * Class to generate class files based on groups and variables extracted from templates files
 */
class ClassGenerator {

    companion object {
        private val generationDirectory = """C:\Users\zert\IdeaProjects\GDOB\src\main\kotlin\com\grimdawnofflinebuilder\util\template\classGeneration\generated"""
    }

    /**
     * Generate the class based on the given mainGroup
     *
     * @param mainGroup the mainGroup to generate class from
     * @param varMap mapping of variables and corresponding groups
     * @param isAbstract true if the class is inherited through include in the template file
     */
    fun generateClass(mainGroup : Group, varMap: Map<Variable, Group>, isAbstract: Boolean ) {
        println( "Generating class for mainGroup ${mainGroup.templateFile?.name}" )
        if( mainGroup.templateFile == null ){
            throw Exception( "Cannot generate class for mainGroup with no template file !" )
        }
        val includes = mainGroup.variables.filter { it.type == "include" }
        var inheritances = includes
          .map { varMap[ it ] }
          .map { getClassName(it ) ?: "" }
          .filter { !it.isBlank() }
          .map { "$it by ${it}Impl( map )" }
          .joinToString( ", " )

        if( !inheritances.isBlank() )
            inheritances = " : $inheritances"

        println( "Group ${mainGroup.templateFile?.name} inherits from $inheritances" )
        val className = getClassName( mainGroup )

        // If class is abstract, generate interface to allow multiple inheritance by mix-ins
        if( isAbstract ){
            val classFile = File( """$generationDirectory\$className.kt""" )
            val writer = classFile.writer()
            writer.use {
                // Package declaration
                writer.appendln( "package com.grimdawnofflinebuilder.util.template.classGeneration.generated" )
                // Imports
                includes
                  .map { varMap[ it ] }
                  .map { getClassName(it ) ?: "" }
                  .filter { !it.isBlank() }
                  .forEach { writer.appendln( "import com.grimdawnofflinebuilder.util.template.classGeneration.generated.$it" ) }
                // Class declaration
                writer.appendln( getClassDeclaration( inheritances, className, true, true ) )
                // Direct variables
                writeVariables( mainGroup, writer, inheritances.isBlank(), true, false )
                // Sub-groups variables
                flattenSubGroups( mainGroup )
                  .forEach { writeVariables(it, writer, inheritances.isBlank(), true, false ) }
                // End of class
                writer.append("}")
            }
        }

        // Then generate implementations
        val implClassName = if( isAbstract ) "${className}Impl" else "$className"
        val classFile = File( """$generationDirectory\impl\$implClassName.kt""" )
        val writer = classFile.writer()
        writer.use {
            // Package declaration
            writer.appendln( "package com.grimdawnofflinebuilder.util.template.classGeneration.generated.impl" )
            // Imports
            if( isAbstract ) {
                writer.appendln("import com.grimdawnofflinebuilder.util.template.classGeneration.generated.$className")
            }
            includes
              .map { varMap[ it ] }
              .map { getClassName(it ) ?: "" }
              .filter { !it.isBlank() }
              .forEach { writer.appendln( "import com.grimdawnofflinebuilder.util.template.classGeneration.generated.$it" ) }
            // Class declaration
            writer.appendln( getClassDeclaration( inheritances, implClassName, false, isAbstract ) )
            // Direct variables
            writeVariables( mainGroup, writer, inheritances.isBlank(), false, isAbstract )
            // Sub-groups variables
            flattenSubGroups( mainGroup )
              .forEach { writeVariables( it, writer, inheritances.isBlank(), false, isAbstract ) }
            // End of class
            writer.append("}")
        }
    }

    /**
     * Flatten the sub-groups hierarchy of the given group into one list
     *
     * @param group the group
     * @return the list of all sub-groups of the given group
     */
    private fun flattenSubGroups( group: Group ) : List<Group> {
        val subGroups = mutableListOf<Group>()
        subGroups.addAll( group.subGroups  )
        subGroups.addAll( group.subGroups
                            .map { flattenSubGroups( it ) }
                            .flatMap { it } )
        return subGroups
    }

    private fun getClassName( group: Group? ) : String? {
        val classNameParts = group?.templateFile?.nameWithoutExtension?.split( "[\\s_]+".toRegex() )
        return classNameParts?.map(String::capitalize)?.joinToString( "" )
    }

    /**
     * @param inheritances List of inherited class
     * @param className class name
     * @param isInterface indicates if the class is abstract
     *
     * @return the declaration of the class
     */
    private fun getClassDeclaration( inheritances: String, className: String?, isInterface : Boolean,
                                     isAbstract: Boolean ) : String
    {
        val classModifier = if ( isInterface ) "interface" else "class"
        val parameters = if ( isInterface ) "" else " ( map: Map<String, String> )"
        val finalInheritances = if( isInterface ){
            "" // Interface cannot inherit by delegation
        } else if( isAbstract ){
            if( inheritances.isBlank() ) {
                ": ${className?.removeSuffix("Impl")}" }
            else {
                inheritances.replace( ":", ": ${className?.removeSuffix("Impl")}," )
            }
        } else {
            inheritances
        }
        return "$classModifier $className$parameters $finalInheritances {"
    }

    /**
     * Writes all variables of the given group to the given output stream
     *
     * @param group group for which to write variables
     * @param writer output to write to
     * @param isRootClass true if the class inherits nothing ( and is inherited )
     * @param isInterface true if the class is an interface, false otherwise
     * @param isAbstract true if the class is to be inherited by delegation
     */
    private fun writeVariables( group: Group, writer: OutputStreamWriter, isRootClass: Boolean, isInterface: Boolean,
                                isAbstract: Boolean )
    {
        if( isRootClass || ( group.type != "system" && group.name != "Header" ) ) {
            writer.appendln("    // Group : ${group.name}")
            group.variables
              .filter { it.type != "include" }
              .forEach {
                  val varNameParts = it.name.split( "[\\s_]+".toRegex() )
                  var varName = varNameParts.map(String::capitalize).joinToString( "" ).decapitalize()
                  if( varName == "class" ) {
                      varName = "clazz"
                  }
                  if( isInterface ){
                      writer.appendln("    /**")
                      writer.appendln("    * ${it.description}")
                      writer.appendln("    * Class : ${it.clazz}")
                      writer.appendln("    * Type : ${it.type}")
                      writer.appendln("    * Value : ${it.value}")
                      writer.appendln("    * Default Value : ${it.defaultValue}")
                      writer.appendln("    **/")
                      writer.appendln("    val $varName : String") // fill in type is String
                  } else {
                      val modifier = if ( isAbstract ) "override " else ""
                      writer.appendln("    ${modifier}val $varName by map")
                  }
                  writer.appendln()
              }
        }
    }

}